WebSocket 客户端开发总结
整个 WebSocket 客户端篇章的核心目标是:构建一个生产级的 WebSocket 连接管理工具,解决从基础连接到多 Tab 共享的各种实际问题。
已完成的功能清单
| 功能模块 | 实现方式 | 解决的问题 |
|---|---|---|
| WebSocketClient 工具类 | class 封装 | 统一管理连接生命周期 |
| 心跳检测 | ping/pong + 定时器 | 检测假死连接 |
| 断线重连 | 自定义策略 + maxRetries | 网络中断后自动恢复 |
| 消息缓存 | dataArr 数组 | 断线期间消息不丢失 |
| SPA 单例模式 | static clients Map | 路由切换不重复创建连接 |
| SharedWorker 跨 Tab 共享 | SharedWorker + useWs hook | 多 Tab 共享一个连接 |
SharedWorker 的局限性
SharedWorker 方案虽然优雅,但存在几个不方便的地方:
- 编译问题:TypeScript 文件需要编译为 IIFE 格式的 JS 才能在 SharedWorker 中使用(
esbuild --format=iife) - 数据传递限制:只能传递可序列化的数据,函数、Error 对象、DOM 元素无法通过
postMessage传递 - retryStrategy 无法传递:因为它是函数,SharedWorker 中只能使用默认的重连策略
- 无法获取 client 实例:
onOpen回调中拿不到 SharedWorker 里的 WebSocket 实例,只能知道连接事件被触发了 - 调试不便:SharedWorker 的日志需要在
chrome://inspect的独立窗口中查看
跨浏览器共享连接的思路
不同浏览器之间是完全隔离的(文件隔离、权限隔离、JS 环境隔离),无法通过客户端方案共享 WebSocket 实例。解决方案需要服务端配合:
- 用户登录后获取 token,建立 WebSocket 连接时携带 token
- 服务端通过 token 识别用户身份,记录 userId 与 clientId 的映射
- 当同一用户建立新连接时,服务端主动断开旧连接
- 通过 timestamp 判断哪个是更新的连接,保留新的、断开旧的
// 服务端伪代码
@SubscribeMessage('message')
handleMessage(client: any, payload: any) {
const userId = payload.userId
const timestamp = Date.now()
// 检查是否有旧连接
const oldConnection = this.userConnections.get(userId)
if (oldConnection) {
oldConnection.client.close() // 断开旧连接
}
// 记录新连接
this.userConnections.set(userId, { client, timestamp })
}
typescript
服务端可以设置更灵活的策略,比如允许同一用户最多 2 个并发连接。
替代方案:visibilitychange + 本地 useWs
如果不使用 SharedWorker,可以用一种更简单的折中方案:
// use-ws-local.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useWsLocal(options: WebSocketClientOptions) {
const client = ref<WebSocketClient | null>(null)
const message = ref(null)
const onMessage = (event: string, data: any) => {
message.value = data
if (options.onMessage) options.onMessage(event, data)
}
const init = () => {
const newOptions = { ...options, onMessage }
client.value = WebSocketClient.getInstance(newOptions)
}
const send = (data: any) => {
client.value?.send(data)
}
const close = () => {
client.value?.close()
}
onMounted(() => {
// 监听 Tab 可见性变化
document.addEventListener('visibilitychange', handleVisibilityChange)
})
onUnmounted(() => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
})
function handleVisibilityChange() {
if (document.visibilityState === 'visible') {
// Tab 被激活,重新加载页面(获取最新数据)
location.reload()
} else {
// Tab 被挂起,断开连接释放资源
close()
}
}
return { init, send, close, message, client }
}
typescript
核心思路:当 Tab 切换到后台时主动断开 WebSocket 连接,切换回前台时重新加载页面。这样做比 SharedWorker 简单得多,但代价是用户切回 Tab 时需要重新初始化。
这种折中方案的考量是双方面的:服务器资源有限性(不活动的连接应该释放)和用户体验(重新加载比保持连接的代价小)。
方案选择建议
| 场景 | 推荐方案 |
|---|---|
| 单页应用,路由切换频繁 | 单例模式(getInstance) |
| 同一浏览器多 Tab,要求实时同步 | SharedWorker + useWs |
| 多浏览器/多设备,允许短暂断连 | visibilitychange + 服务端踢旧连接 |
| 高安全性场景(金融、医疗) | 服务端强管控,单用户单连接 |
↑